Lua


Author
|
Earl
Describe
|
该文档列举Lua的基本语法、在Web开发中使用Lua脚本的常用场景和注意事项
Reference
|
Last Update
|
2024-8-15

 

Lua基础语法

  1. 使用LDT创建Lua项目

    • 创建新Lua项目

    • 设置项目名称和Luajit版本

      • 🔎:项目名称随意,版本使用默认的5.1即可

 

main函数



  1. main方法定义示例

    • 这也是定义函数的方法,local是作用域,function表示这是一个函数,main是函数名,括号中可以接参数,括号到end间的区域就是写代码的地方

      • 🔎:lua的代码块不像java一样使用花括号来表示一个代码块的结束,也不像Python一样使用严格的缩进对齐方式来表示代码片段等级,而是通过特定的关键字开启代码块,以关键字end作为代码块的结束,如果没有end代码块就不知道在何处被结束

    • 最下面的main()是去调用上面定义的main方法

    • 在LDT中运行Lua脚本点击绿色的运行按钮--run as--lua Application

     

关键字



  1. 关键字列表

    • nil是空值,表示访问的是一个没有声明过的变量,相当于java和c语言中的NULL

    andbreakdoelse
    elseifendfalsefor
    function ifinlocalnil
    notorrepeatreturn
    thentrueuntilwhile

 

变量


  1. 作用域

    • 变量声明前加local关键字是局部变量如local a=5

      • 🔎:局部变量只能在代码块内部进行使用

    • 变量声明前没有任何特殊说明全是全局变量如a=5

      • 🔎:函数或者语句块中的定义的变量没有local关键字都是全局变量,可以在任何地方进行调用

  2. 变量赋值

    • 可以多个变量同时赋值,根据顺序进行匹配

    • 数量对不上也不会报错,多出来的字面值不会用上;

    • 字面值少了多出来的参数会赋值控制nil

     

 

注释

  1. 单行注释

  2. 多行注释

 

数据类型



数字

  1. 数字类型变量定义示例

    • 数字变量直接写数字,不用写具体类型,Lua语法中的变量都是弱类型的,只有执行的时候才知道数字是那种类型的

      【数字类型实例】

字符串

  1. 字符串类型变量定义示例

    • 字符串字面值可以用单引号,也可以用双引号,还可以使用两个中括号

      • 🔎:两个中括号的字符串可以是多行字符串,而且貌似里面的特殊符号不需要转义

    • 字符串中可以使用转义字符\n 【换行】、 \r 【回车】、 \t 【横向制表】、 \v 【纵向制表】、 \\ 【反斜杠】、 \” 【双引号】、 以及 \'【单引号】等等

    • lua拼接字符串和变量需要在变量前面使用..,java是在变量两边用+

    【字符串类型实例】

    【字符串可以带换行效果】

布尔类型

  1. 布尔类型

    • false可以用falsenil表示

    • true可以使用数字0和空字符串'\0'表示

 

Table

  1. Table类型定义示例

    • Table就像java中使用的map,即key-value的形式存储数据

    • dog就是一个Table,并不是一个Map,因为Table中除了键值对还能放一些额外的东西【感觉Table有点像对象,但是Lua不支持面向对象,只能通过Table模拟出来看起来好像是面向对象的方式】,dog也能直接打印【打印的是Table的内存地址,没有toString()

    • dog中可以通过key获取到值并给对应的key赋值或者获取更改Table中的值

 

数组

  1. 数组类型定义和使用示例

    • 数组只有value,没有key;依靠下标去取value;数组的value还可以是自定义函数,而且还是匿名的【牛皮】;而且匿名函数只会在取value并以方法调用的方式才会执行。arr[4]可以看做匿名函数的方法名

    • 数组的下标是从1开始的【Lua的下标都是从1开始】

  2. 数组遍历

    • 使用for k,v in pairs(数组变量) do函数可以将数组拆成key-value的形式,k是下标,v是对应下标的值;每次循环是按顺序取值并赋值给k,v

 

流程控制



  1. 语法格式

    • 🔎:注意布尔表达式两边不用加小括号

  2. 代码示例

    • 定义一个年龄140,性别男;

    • 如果年龄为40且性别为男则打印男人四十一枝花【很多脚本语言的逻辑判断都是if...then,then后面是匹配上条件以后执行的代码,且C语言系的if后面要加括号,VBScript这一系的if后面不需要加括号】;

    • 中间的其他逻辑判断选项使用elseif...then连接,相当于java中的else if;【~=意思是不等于】

    • 最后总的兜底逻辑判断用else衔接;【lua拼接字符串和变量需要在变量前面使用..,java是在变量两边用+号】

    • 循环体的最后用end结束

 

 

 

函数

打印函数

  1. print(String)print(name,bol)

    • print函数是Lua的内置函数,向终端控制台打印字符串,是换行打印;如果用逗号隔开是打印两个变量值,用制表符【不像空格,比空格宽很多】隔开

 

循环

  1. while循环

    • 定义局部变量i为0,局部变量max为10,当i小于等于10时执行打印i并让i加1

    • end是循环体的end,和函数的end不是同一个end

  2. for循环

    • 语法格式:

      • 🔎:变量varexp1变化到exp2,每次变化以exp3为步长递增var,并执行一次 "执行体"exp3是可选的,如果不指定,默认为1

    • 代码示例:

      • 🔎i初始值为100,想要i<1,每次循环i每次减去2;i = 1, 100, 2表示i等于1,i小于100,每次循环i加上2

 

 

自定义函数

  1. 自定义函数示例

    • 定义一个名为myPower的函数,下面的演示是在main函数中又定义了一个myPower函数,作用是两个参量之和,后面调用函数并传参,将结果传递给变量power2并打印

    • 函数可以嵌套定义

    • 千万别把最后的main()漏了,不然程序压根不会执行

 

匿名函数

  1. 匿名函数示例

    • 在下面的函数newCounter()中定义匿名函数【匿名函数没有名字】,newCounter()返回的是匿名函数,匿名函数的第一次执行结果其实就是1,

    • 这种写法类似于javaScript中的闭包,c1是获取匿名函数【本次获取不会进行一次计算,只会为匿名函数的变量赋初始值,这个变量初始值作为全局变量会累加,此时只是执行匿名函数,并没有执行local i = 0】,此后的c1()会执行匿名函数并且将结果返回,其中匿名函数的变量i累加1;执行一次就会累加一次【理解成执行c1 = newCounter()就将newCounter()返回的匿名函数以c1作为方法名了,匿名函数中的变量i是c1函数中完全独立的变量【看做是完全独立的变量】,又是全局变量且没有初始化动作,所以每次执行都会累加】

    【执行效果】

     

成员函数

  1. 成员函数示例

    • person是一个Table,可以在Table中自定义没有的方法,通过Table名.函数名()对成员函数进行调用

 

函数返回值

  1. 函数返回值

    • 就是return的值可以return两个甚至多个值,java就不行,需要搞到集合、数组或者对象里面返回,在拉出来分割

     

Redis中使用Lua

EVAL指令

  1. EVAL script numkeys key [key...] arg [arg...]指令详解

    • 参数script:Lua脚本字符串,该脚本应该被处理成不带格式的一行

    • 参数numkeys:KEYS列表的元素个数

      • 🔎:该参数在EVAL指令中必须传递,否则报错

    • 参数[key...]:KEYS列表,元素以空格进行分隔,在参数script中通过参数KEYS[下标]获取,下标从1开始

    • 参数[arg...]:ARGV列表,元素以空格进行分隔,在参数script中通过参数ARGV[下标]获取,下标从1开始

  2. EVAL指令使用注意事项

    • 使用EVAL指令必须传递numkeys的值,即KEYS列表的数量

      • 🔎:如果完全不传递numkeys执行EVAL指令就会直接报错(error) ERR wrong number of arguments for 'eval' command

    • Redis中的Lua脚本,控制台不会输出脚本的打印指令print函数中的值,而是输出Lua脚本return的返回值

      • 🔎:没有返回值会直接输出nil,有返回值即使没有print函数也会直接在控制台打印返回值

    • Redis不允许用户去声明全局的lua脚本变量,只允许声明局部变量

    • 在Eval指令中使用流程控制语句

    • 在EVAL指令中通过key列表和arg列表向流程控制语句传递参数,示例如下

      • 🔎numkeys用于指定KEYS列表中元素的个数,比如numkeys的值为2,表示后面指令的参数前2个是KEYS列表,后面的都是ARGV列表,列表中的元素通过列表名[下标]进行引用,两个列表的下标都是从1开始的

      • 🔎:KEYS列表和ARGV列表中的参数无需全部是使用,不使用也不会报错

      • 🔎:下面示例的含义是如果KEYS列表的第一个元素大于ARGV列表的第一个元素就返回KEYS列表的第二个元素,否则就返回ARGV列表的第二个元素

    • 在EVAL指令中返回多个参数值

      • 🔎:注意EVAL指令中只有用花括号括起来的多个值才能被全部打印,没有花括号括起来的值只会打印第一个

  3. EVAL指令中的Lua脚本使用redis指令

    • 🔎:lua脚本可以保证Redis多条指令原子性的原理是Redis客户端程序通过lua脚本把多个Redis指令一次性发送给Redis服务器,那么这些指令就不会被其他客户端指令打断。Redis的单线程设计也会保证脚本以原子性的方式执行即当某个脚本正在运行的时候,不会有其他脚本或Redis命令被执行。

    • 要在参数script的lua脚本中使用redis命令需要在lua脚本中调用Redis主动向Lua脚本暴露的一个类库redis的call方法来执行redis中的指令,相应的格式为EVAL "redis.call('get','<key>')" 0

    • lua脚本中的所有redis指令都通过redis.call()进行调用,call方法中的参数和redis的实际指令是完全一样的顺序,用逗号填充redis命令中间的空格就是对应的call方法参数列表,如

    • 在lua中使用redis指令一般都是嵌入到应用程序中做复杂业务比如实现分布式锁,此时key和value以及命令参数都是变量,此时就可以配合KEYS列表和ARVG列表来动态地传递redis命令的参数

  4. EVAL指令中使用Lua脚本来保证多个redis命令的原子性

    • 这个是配合基于Redis实现的分布式锁最终解锁时通过lua脚本来保证验证键值对的value值与获取锁设置的value值一致并在一致的情况下删除键值对释放锁两个操作的原子性的代码

      • 🔎:当redis中没有对应value为12345678,key为lock的键值对时就直接返回0,如果有就删除对应的键值对,del命令删除成功会自动返回1,删除失败返回0

      • 🔎:经过测试,该lua脚本没有问题;只有当key和value都为指定值时才删除对应键值对并返回1,当对应key的键值对不存在或者键值对存在但是value值不是指定值,就直接返回0

 

基于Redis的分布式锁



 

  1. 比较完善的基于Redis的分布式锁实现如下

    • 分布式锁DistributedRedisLock

    • 工厂方法获取分布式锁对象

    • 业务方法使用分布式锁示例

      • 业务逻辑是并发请求对100个用户线程,1s内每个用户线程发起50次扣减库存请求,总共发起5000次,操作虚拟机上的同一个Redis数据库的同一个共享数据,对库存数量5000进行单次扣减1,累计5000次扣减请求,使用基于Redis的分布式锁解决Redis中共享库存数据的并发线程安全问题

  2. 该分布式锁实现了以下特性

    • 通过Redis的setnx指令做到分布式锁的独占排他,后面用lua脚本一次提交执行和Redis单线程特性保证原子性并结合lua脚本的逻辑判断替换了setnx指令的独占排他

    • 通过设置过期时间来防服务宕机或者意外锁无法释放导致的死锁现象,使用完整的set指令来保证独占排他并同时设置有效时间保证上锁和设置有效时间两步操作的原子性,最后被lua脚本整合到同时实现上锁和锁重入的hincrby指令和Expire指令中,用Lua脚本保证上锁或锁重入与设置有效时间两步操作的原子性

    • 通过Redis中的Hash数据类型,以key作为锁的唯一标识,以key加field字段【当前线程创建的UUID】作为线程自身上锁的唯一标识来防止当前线程误删其他线程上的锁,避免因为锁提前失效或者一系列其他原因导致的非上锁线程执行锁释放操作;后为了在定义方法时就确定同一个线程锁重入的自动识别,uuid很难实现在方法定义时就保证方法锁重入时两把锁的uuid相同,因此将当前线程标识即field字段重新设计为线程id,为了避免集群环境下不同服务实例的线程id相同导致上锁通过锁重入获取锁导致锁失效问题,使用uuid作为服务的唯一标识,field字段使用uuid:线程id结合key作为区分获取锁的当前线程的唯一标识

    • 使用lua脚本保证加锁和设置锁过期时间、判断锁是当前线程上的锁和释放锁、判断锁是当前线程上的锁和为锁续期多步操作的原子性

    • 分布式锁的不可重入也可能会导致死锁,用Hash数据类型,key作为锁唯一标识,key和field【uuid:线程id】作为当前线程的唯一标识来做锁重入、锁释放和锁续期中锁属于当前线程的判断标识、以value作为锁重入次数的计数,该设计模仿可重入锁的锁重入实现方式,用lua脚本保证检查锁和操作锁多步操作的原子性

    • 使用JDK的Timer定时器和lua脚本实现可重入锁的自动续期

     

 

Nginx中使用Lua

OpenResty



 

Lua脚本的使用

  1. 在OpenResty中使用lua脚本

    • 🔎:lua脚本写在Nginx的配置文件中

    • 使用方式一

      • 在nginx.conf中添加一个location写lua脚本,location中除了lua就没有其他东西了,没有proxy_pass或者root根目录,全是由Lua代码负责输出内容

        • 🔎:这种方式存在一个弊端,因为配置文件主要是做配置用的,不是拿来做编程用的;一旦lua代码多了,配置文件会非常庞大,这就引申出第二种方式在配置文件nginx.conf中引入Lua脚本

      • 测试效果

    • 使用方式二

      • 在nginx.conf的location下使用命令content_by_lua_file lua脚本的相对或绝对路径;引入lua脚本,Lua脚本的根目录在nginx的主目录下

      • 操作步骤:

        • 在nginx的根目录下创建lua目录,在lua目录下创建lua脚本文件hello.lua,向脚本文件中写入以下内容

          【hello.lua】

        • 修改配置文件nginx.conf,将原来lua脚本直接写入配置文件的站点目录改为在站点目录中引入lua脚本

          • 🔎:经过测试,因为期间lua脚本的相对路径写错了,导致页面不展示,更改以后正常展示,由此证明实现了相应的效果

          • 🔎:注意因为还是在配置文件中引入了lua脚本,所以更改了lua脚本还是需要重启Nginx

      • 测试效果

  2. 开启lua脚本热部署

    • 因为每次修改lua脚本以后都需要重启nginx,很不方便,在http模块下使用命令lua_code_cache off;来开启lua脚本的热部署

      • 🔎:这个热部署生产环境不要开,对性能影响比较大;

      • 🔎:设置该指令后关闭nginx会提示nginx: [alert] lua_code_cache is off; this will hurt performance in /usr/local/openresty/nginx/conf/nginx.conf:13,意思是设置该指令会损伤nginx的性能;这是因为lua在OpenResty中去跑是介于脚本语言和静态编译语言之间的动态语言,性能比较高,但是如果编程纯粹的脚本语言或者解释型语言,每次请求的时候都会去重新加载执行一遍,浪费性能【没听懂,以后理解】;但是开发的时候方便程序员去重启nginx

      • 🔎:这个热部署只是针对lua脚本文件修改,修改了主配置文件还是需要重启nginx

     

Lua脚本获取系统变量


  1. Lua获取请求头信息

    • local headers = ngx.req.get_headers():获取当前请求的头信息

    • headers["Host"]:请求的ip和端口,key不区分大小写

    • headers["user-agent"]:请求的客户端信息【也可以通过headers.user_agent获取对应的属性值】

    • 脚本hello.lua

    • 执行效果

      • 🔎:实际上headers中的所有参数从第4行到11行一共八个参数

  2. lua处理Http请求的其他方式

    • 🔎:没说怎么用,只是说除了content_by_lua_file还可以使用以下命令修改系统变量,猜测是和content_by_lua_file一样使用在location中

    • set_by_lua

      • 参数解析:修改nginx中的系统变量

    • rewrite_by_lua

  1. Lua获取Post请求的请求参数

    • hello.lua

      🔎:核心还是通过local post_args = ngx.req.get_post_args()获取到Post请求的请求参数列表然后对请求参数列表进行遍历

    • 执行效果

      • 🔎:这个需要发送post请求才能看见效果,get请求参数列表为空,但是不会报错,使用postman进行测试,注意参数要放在请求体中的表单去提交

    • postman请求参数设置

  2. Lua获取uri中的单一变量

    • nginx.conf

  3. Lua获取请求uri中的所有变量

    • hello.lua

  4. Lua获取请求的通用信息

    • 🔎:以下代码全部在hello.lua脚本中

    • 获取http协议版本ngx.req.http_version()

    • 获取请求的方法ngx.req.get_method()

    • 获取原始的请求头内容ngx.req.raw_header()

      • 🔎:最原始的整个请求头文本

    • 获取请求的请求体内容ngx.req.get_body_data()

       

Lua做Nginx进程缓存


  1. 方式一:在lua脚本中使用lua_shared_dict

    • 🔎shared_dict性能比较高效,在多个worker进程中可以去共享一份缓存数据,因为多进程操作一份缓存数据,一定会涉及到锁的产生,有点像在nginx中跑一个小型的redis

    • 核心一:在lua脚本中使用变量shared_data = ngx.shared.shared_data来操纵缓存数据

    • 核心二:同时使用shared_dict需要在主配置文件的http模块中声明shared_dict和其大小

    • hello.lua

    • nginx.conf

      • 🔎:声明shared_dict的空间大小,表示申请1M内存去做进程间的内存缓存,该内存缓存能够被所有的worker进程进行访问并且能保证原子性

    • 请求访问效果

      • 🔎:这是从缓存空间取出的数据,中间还涉及到使用Lua代码对key为i的数据的创建和自增操作

  2. 方式二:使用lua-resty-lrucache模块做内存缓存

    • LRU缓存对比shared_dict的功能更加强大一些,该模块纯粹用Lua语言实现的,由Lua官方提供的模块,也可以像shared_dict一样在nginx的内存中使用缓存,且运行在独立的进程中,单进程的增删改操作不需要添加锁,做一些修改操作的时候性能会更高一些,但是一般使用时不会感觉和shared_dict在性能上的差异

    • LRU缓存可以额外做LRU算法上的清理工作;同时LRU缓存是以key-value的数量个数作为大小的限制、shared_dict是以缓存空间的内存占用大小来作为限制,shared_dict能更有效地控制系统内存缓存空间的大小占用,LRU没法预估内存缓存空间的大小,这种大小的限制需要对每一个key-value的大小需要提前预测感知才行,但是内存也更加弹性化,不会导致因为内存空间占满了导致数据写不进去

    • lua-resty-lrucache默认就已经在OpenResty的目录下了,资源文件路径/usr/local/openresty/lualib/resty/lrucache.lua

    • LRU缓存使用时一定要关闭lua脚本的热部署,即不能使用lua_code_cache off;,默认是on

    • 官方推荐的用法是在lua脚本中自定义函数来使用,使用示例如下

      • 🔎:一定要保证初始化操作只能被执行一次,包括连接mysql和redis的数据库连接池时也是一样只能执行一次,否则是用不上此前使用过的连接或者缓存空间的,Lua如何保证只执行一次某个代码片段没有讲,这里后续自己学习【还没讲呢,这里第一次演示的就是每次请求都执行一次初始化代码的情况】实际上这里的代码确实只会执行一次,这个也是全局变量,但是因为在主配置文件开启了lua脚本的热部署,lua_code_cache off;关闭了lua代码的缓存,每次请求结束lua代码创建的全局参数缓存都会不使用

    cache.lua

    • 🔎:该文件在/usr/local/openresty/lualib/my/cache.lua

    在主配置文件nginx.conf中使用content_by_lua_block来对lua函数进行调用

    require引入脚本文件不存在的位置报错提示信息

    • 🔎:这里找文件my/cache,通过访问/lua站点,因为没有该文件,错误日志会显示在所有可能目录中没有对应的文件

    • 默认的lua文件可以存在的地方的根目录如下所示,lualib也是一个根目录

    更改Lua脚本的根目录

    • Lua脚本的根目录也可以通过在主配置文件的http模块下通过以下配置设置,这是将lua脚本的目录修改到绝对路径/path/to/lua-resty-lrucache/lib/

      • 🔎:上面的演示不采用这种方式,选择在lualib目录下创建my目录,在该目录下存放自定义的lua脚本文件

    执行效果

    • 🔎count=init是文件cache.lua通过代码ngx.say("count=init")打印的日志,通过测试发现目前每次请求都会去执行一次cache.lua文件中的代码,c, err = lrucache.new(200)创建缓存空间的代码后面紧跟的就是打印count=init的代码,每次请求都打印了,说明每次请求都新创建的缓存空间,该问题需要解决,否则缓存空间根本没有办法使用

    • 将主配置文件的lua_code_cache off;注释掉观察多次请求的效果

      • 🔎:原因就是lua_code_cache off;禁用Lua脚本缓存,导致require("my/cache")执行了一次该文件返回的对象无法被缓存,从而每次请求都会去执行一次初始化代码,实际生产环境中不会开启该热部署选项

      • 🔎:以下是注释掉该热部署配置后的响应效果

 

连接Redis



 

  1. 使用OpenResty去连接redis

    • 🔎:好像没有单独引入lua-resty-redis模块,而是OpenResty默认集成了该工具,通过lua命令local redis = require "resty.redis"使用的

    • 创建redis连接的lua脚本文件/usr/local/openresty/nginx/lua/redis.lua

    • 主配置文件nginx.conf

    • lua脚本执行效果

 

 

连接mysql


  1. 使用OpenResty连接mysql

    • 创建mysql连接的lua脚本文件/usr/local/openresty/nginx/lua/mysql.lua

      • 🔎:这里需要修改mysql的权限,否则连接会失败;mysql client 命令行输入下列指令设置mysql的权限把连接权限改为任何地址

    • 主配置文件nginx.conf

    • 响应效果

      • 🔎:没有t_emp数据,这里直接报错该表不存在结束脚本执行了

      • 课堂实例

模板引擎


  1. 使用模板引擎对标签进行渲染

    • 🔎:模版引擎还有很多其他的配置,能够让页面变得更加复杂,这里只是简单介绍

    • 在OpenResty的根目录下创建模板目录tpl,在模板目录下创建模板文件view.html

      • 🔎:模板由html和标签【标签用两个大括号括起来】组成

    • 在主配置文件中配置模板文件存放位置

      • 🔎:在location下进行模板文件根目录配置set $template_root /usr/local/openresty/tpl;,引入Lua脚本文件content_by_lua_file lua/tpl.lua;,表示在文件tpl中渲染模板

    • 安装lua-resty-template模块

      • 默认OpenResty下没有该模块,下载2.0版本lua-resty-template-2.0.tar.gz;将文件上传到lualib目录下并解压缩

      【解压目录结构】

      • lua-resty-template-2.0/lib/resty/目录下的所有文件挪到lualib/resty目录下

        • 🔎:将这两个文件挪动到/usr/local/openresty/lualib/resty目录下,挪动后剩下文件lua-resty-template-2.0可以直接删掉

    • /usr/local/openresty/lua/目录下创建lua脚本tpl.lua

    • 访问效果

  2. 模版引擎的基本用法

    • 🔎:讲的很粗糙,细节看官方文档学习,弹幕说这个语法和django很像

    • 模板文件/tpl/view.html

    • nginx/lua/tpl.lua

    • 主配置文件nginx/conf/nginx.conf

    • 测试效果

      • 🔎:因为这里没有header.htmlfooter.html,这两文件一般放不咋变化的头和尾,因为找不到对应的文件,所以直接把文件名打印出来了

      • 🔎header.htmlfooter.html都是模板文件,根目录都是和主模板文件view.html在同一个模板根目录tpl

    • tpl/header.html

    • tpl/footer.html

    • 头尾拼接效果

       

       

OpenResty应用示例



 

  1. 搭建流程

    • tpl.lua

      • 🔎:这里需要创建表t_temp,还要插入数据,换个谷粒学院的edu_chapter表来进行实现

      • 🔎:这个sql是针对请求写死在脚本中的,用户只知道点了按钮,不知道sql

    • view.html

    • 测试效果

    • 使用的sql